package com.nulldev.util.web.dns.server.server;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.other.ArrayUtils;
import com.nulldev.util.threading.v2.ThreadManager;
import com.nulldev.util.threading.v2.ThreadPriority;
import com.nulldev.util.threading.v2.nThread;
import com.nulldev.util.threading.v3.RunnableWorker;
import com.nulldev.util.threading.v3.ThreadDispatcher;
import com.nulldev.util.web.dns.server.message.DNSResponseBuilder;
import com.nulldev.util.web.dns.server.message.DNSResponseCode;
import com.nulldev.util.web.dns.server.server.connector.DNSConnector;
import com.nulldev.util.web.dns.server.server.engine.DNSEngine;

public class DNSServer {
	private final Logger logger = LoggerFactory.getLogger(DNSServer.class, true ,true);

    private volatile boolean running = false;

    private ConcurrentMap<DNSConnector, ConnectorWorker> connectorWorkers = new ConcurrentHashMap<DNSConnector, ConnectorWorker>();
    private DNSEngine[] engines;
    //private Executor executor;
    private AtomicLong connectorIndex = new AtomicLong(0);
    
    private final ThreadDispatcher tm = new ThreadDispatcher(true);

    /**
     * Starts the server. After this method is invoked, all connectors begin reading messages and passing them to the
     * engine for processing. If an {@link java.util.concurrent.Executor} has not been set, a
     * {@link java.util.concurrent.ThreadPoolExecutor} will be created by default.
     * @throws java.lang.IllegalStateException If the server is already running
     */
    public void start() {
        logger.info("Starting DNS Server");

        if (running) {
            throw new IllegalStateException("Server is already running");
        }

       // if (null == executor) {
       //     logger.debug("No Executor found, creating ThreadPoolExecutor");
       //    // ThreadPoolExecutor tpExec = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS,
       //    //         new ArrayBlockingQueue<Runnable>(Integer.MAX));
       //     logger.debug("Prestarting all cores in ThreadPoolExecutor");
       //    // tpExec.prestartAllCoreThreads();
       //     executor = Executors.newCachedThreadPool();
       // }

        for (ConnectorWorker worker : connectorWorkers.values()) {
            logger.debug("Starting connector thread {}", worker.getName());
            tm.addTask(RunnableWorker.fromRunnable(worker, "$nDNS.ConnectorWorker"));
        }

        running = true;
    }

    /**
     * Indicates whether the server is currently running.
     * @return
     */
    public boolean isRunning() {
        return running;
    }

    /**
     * Adds a connector. If the server has already been started, the worker thread will begin reading from the connector
     * immediately.
     * @param connector
     */
    public void addConnector(DNSConnector connector) {
        logger.debug("Adding connector to server");

        long index = connectorIndex.incrementAndGet();
        ConnectorWorker worker = new ConnectorWorker(connector, "connector-" + index);
        ConnectorWorker oldWorker = connectorWorkers.putIfAbsent(connector, worker);

        if (null != oldWorker) {
            worker = oldWorker;
        }

        if (!worker.isAlive() && !worker.isShutdown() && running) {
            logger.debug("Server is already running, starting connector thread");
            worker.start();
        }
    }

    /**
     * Method for adding a list of connectors to the server. This also makes the component more Spring dependency
     * injection friendly.
     * @param connectors Connectors to add to server
     */
    public void setConnectors(List<DNSConnector> connectors) {
        for (DNSConnector connector: connectors) {
            addConnector(connector);
        }
    }

    /**
     * Remove a connector from the server. If the server has already been started, this will interrupt the worker thread
     * that sends and receives messages through this connector.
     * @param connector
     */
    public void removeConnector(DNSConnector connector) {
        logger.debug("Attempting to remove connector");
        ConnectorWorker worker = connectorWorkers.remove(connector);

        if (null != worker && !worker.isShutdown()) {
            logger.debug("Stopping connector thread {}", worker.getName());
            worker.shutdown();
        }
    }

    /**
     * Stops the server and interrupts all connector threads.
     */
    public void stop() {
        logger.info("Stopping DNS Server");

        if (!running) {
            logger.warn("DNS Server is already stopped");
        } else {

            running = false;

            for (ConnectorWorker worker : connectorWorkers.values()) {
                logger.debug("Stopping connector thread {}", worker.getName());
                worker.shutdown();
            }

            logger.debug("Stopping Executor");
        }
    }

    private class ConnectorWorker extends Thread {

        private DNSConnector connector;
        private volatile boolean shutdown = false;

        public ConnectorWorker(DNSConnector connector, String name) {
            this.connector = connector;
            this.setName(name);
        }

        @Override
        public void run() {

            while (!shutdown) {
                if (connector.isBlocking()) {
                    try {

                        final DNSPayload payload = connector.read();

                        try {
                            if (null != payload) {

                                logger.debug("DNS query received from {}", payload.getRemoteAddress());

                                tm.addTask(new nThread(new Runnable() {
                                    @Override
                                    public void run() {
                                        DNSPayload response = ArrayUtils.random(engines).processDnsPayload(payload);
                                        try {
                                            logger.debug("Sending response to {}", payload.getRemoteAddress());
                                            connector.write(response);
                                        } catch (IOException e) {
                                            logger.error("Connector write error", e);
                                        } catch (Exception e) {
                                            logger.error("Connector error", e);
                                        }
                                    }
                                }, "$nDNS.task", ThreadPriority.NORMAL).toWorker());
                            }
                        } catch (RejectedExecutionException e) {
                            logger.error("Executor cannot accept any more tasks", e);
                            connector.write(returnServerFailure(payload));
                        } catch (Exception e) {
                            logger.error("Error resolving response", e);
                            connector.write(returnServerFailure(payload));
                        }
                    } catch (IOException e) {
                        logger.error("Error reading from connector", e);
                    }
                } else {
                    // uhhhh shit
                }
            }
        }

        private DNSPayload returnServerFailure(DNSPayload payload) {
            DNSResponseBuilder builder = new DNSResponseBuilder(payload.getMessageData());
            builder.setResponseCode(DNSResponseCode.SERVER_FAILURE);
            payload.setMessageData(builder.build());
            return payload;
        }

        public boolean isShutdown() {
            return shutdown;
        }

        public void shutdown() {
            this.shutdown = true;
        }
    }

    /**
     * Get the engine used to process DNS messages.
     * @return
     */
    public DNSEngine[] getEngines() {
        return engines;
    }

    /**
     * Set the engine used to process DNS messages.
     * @param engine
     */
    public void setEngines(final DNSEngine... engines) {
        assert null != engines && engines.length > 0;
        this.engines = engines;
    }

//    /**
//     * Get the {@link java.util.concurrent.Executor} response for processing requests concurrently.
//     * @return
//     */
//    public Executor getExecutor() {
//        return executor;
//    }

//    /**
//     * Set the {@link java.util.concurrent.ExecutorService} used to process requests concurrently.
//     * @param executor
//     */
//    public void setExecutor(Executor executor) {
//        assert null != executor;
//        this.executor = executor;
//    }
}
